ICTSC2020 k8s運用解説 後編:運用編

こんにちは。@takemioIOです。
この記事は ICTSC2020 k8s運用解説、前編:構築と構成の後編 にあたります。
ここでは以前の記事ではk8sを構築したことについてと全体像について述べました。ここではその構築したk8sをどのように利用したのか、それらを通じた知見を述べます。

CDについて

こんにちは。CI/CDに関する部分を担当した sakuraiです
今年はArgoCDHelmfileを利用してクラスタごとにスコアサーバー(コンテストサイト)などをデプロイする運用をしました。

背景

クラスタチームではKubernetesクラスタを3つ運用し、次のような位置づけとしています。

  • wspクラスタ(監視系、ダッシュボードなど)
  • devクラスタ(開発用テスト環境)
  • prdクラスタ(コンテスト参加者へ提供する本番環境)

各クラスタへのアプリケーションのデプロイを行うためには、そのクラスタに対してkubectl apply -f hogehoge.yamlといったコマンドを打つ必要があります。しかし、これを手作業で行うことは

  • 単純に手間
  • 人為的なミスが起こりうる
  • アプリケーション側の人間がクラスタを触る必要がある

ということがあります。そこで、クラスタへのデプロイを自動化したりクラスタチーム以外がk8s上にアプリケーションをデプロイするときの動線を整備したりすることによって、k8sとその上のアプリケーションの運用を継続的に行えることを目指しました。

Helmfile

まず初めに、これまでスクリプトでごり押されていたマニフェスト群をテンプレート化を行いました。テンプレート化にはKustomizeとHelmが候補となりましたが、環境変数の変更のしやすさの観点からHelmを使用することにしました。また、要件としてアプリケーションを各クラスタへデプロイする際、環境変数を変えることでConfigMapやimageを変更できる必要がありました。このテンプレート化では例えば、

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: {{ .Values.__VAR__NAMESPACE }}
...

values.yaml

__VAR__NAMESPACE: "scoreserver"

というようにマニフェストの一部を変数として、その変数に対応する内容を記したファイルを用意することで目的のマニフェストを出力することができます。

# helm template --values values.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: scoreserver
...

さらに、この変数の内容(values.yaml)を各クラスタと紐づけるためにHelmfileを利用しました。HelmfileではHelmのテンプレートに加えて環境を定義することができます。ここでdevelop/productionというように環境を定義し、環境変数も紐づけていきます。

helmfile.yaml

environments:
  workspace:
    values:
    - environment/workspace/values.yaml
  develop:
    values:
    - environment/develop/values.yaml
  production:
    values:
    - environment/production/values.yaml
...

単純な置き換えとしては次のようになります。

helm template --values environment/production/values.yaml

↓

helmfile --environment production template .

Argo CD

クラスタへのアプリケーションデプロイの自動化ツールとして、Argo CDを利用しました。Argo CDはGitHub上のマニフェストを参照して、アプリケーションのデプロイを行うことができます。また、GitHub上でそのマニフェストが変更された場合に自動でデプロイを行うことができます。したがって、Argo CD上でデプロイ設定を行った後はGitHub上でマニフェストを管理することでデプロイを行うことができるため、アプリケーション管理者がクラスタへ触る必要がなくなります。

(Argo CD自体の導入はとても簡単なので内容としては割愛します。GUIで設定できるし適当に使いたいときもおすすめできそう。)

Argo CDはhelmなどのテンプレートエンジンに対応していますが、Helmfileには対応してないためプラグインとして追加します。

deploy.yaml

spec:
  template:
    spec:
      # 1. Define an emptyDir volume which will hold the custom binaries
      volumes:
      - name: custom-tools
        emptyDir: {}
      # 2. Use an init container to download/copy custom binaries into the emptyDir
      initContainers:
      - name: download-tools
        image: alpine:3.8
        command: [sh, -c]
        args:
        - wget -qO /custom-tools/helmfile https://github.com/roboll/helmfile/releases/download/v0.128.0/helmfile_linux_amd64 && chmod +x /custom-tools/*
        volumeMounts:
        - mountPath: /custom-tools
          name: custom-tools
      containers:
      - name: argocd-repo-server
        volumeMounts:
        - mountPath: /usr/local/bin/helmfile
          name: custom-tools
          subPath: helmfile
kubectl -n argocd patch deploy/argocd-repo-server -p "$(cat deploy.yaml)"

アプリケーション情報の登録はCUIまたはGUIで行うことができるが、こちらもテキストで管理が可能です。

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: scoreserver-production
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: production
  source:
    repoURL: /* CENSORED */
    path: scoreserver
    targetRevision: master
    plugin:
      name: helmfile
      env:
        - name: ENV
          value: production
  destination:
    name: kubernetes-prd #ここでクラスタを指定する。Argo CDが動いているクラスタ以外は別途事前登録する必要がある。
    namespace: scoreserver-production 
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Argo CDにはadminユーザがデフォルトで設定されていますが、ユーザの追加を行ったり権限設定を行うことが可能です。(設定方法)
ICTSCではprdクラスタへの変更を制限して、本番環境への破壊や意図しない変更を防ぐようにしています。

Argo CD notifications

デプロイ結果をSlackに通知するためにArgo CD notificationsを使いました。
(比較的最近v1.0~になって変更が辛かったデスネ)

ドキュメントがアレなんですが、最低限以下だけすれば動きそうです。(別途Slack側でOauth tokenの発行は必要)

# kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-notifications/v1.0.2/manifests/install.yaml
# kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-notifications/v1.0.2/catalog/install.yaml
# kubectl edit cm -n argocd argocd-notifications-cm

data:
  defaultTriggers: |
    - on-deployed
    - on-health-degraded
    - on-sync-failed
    - on-sync-running
    - on-sync-status-unknown
    - on-sync-succeeded
# kubectl edit secret -n argocd argocd-notifications-secret

stringData:
  slack-token: CENSORED

以下のような通知が届くようになります。

監視基盤

こんにちは、監視基盤を担当した梅田@x86takaです。

今回は監視基盤として行ったことを説明します。

まず初めに、今年の監視基盤の構成を説明します。

構成

まず、最初に監視対象です。

監視対象はNTT中央研修センタをメインに行いました。

  • NTT中央研修センタ
    • サーバ
      • S7 * 2
      • m4 * 2
      • RH1288 * 4
    • ネットワーク機器
      • MX5
      • SRX1500
      • SN2410
  • さくらクラウド
    • k8sクラスタ
      • prd クラスタ
      • wsp クラスタ

NTT中央研修センタとさくらクラウドの構成は、前回の記事に詳細にかかれています。

また、これらの監視に利用したコンポーネントは以下の通りです。

  • 分析
    • Elasticsearch
  • データ収集
    • Logstash
    • Prometheus
    • Elastiflow
    • Zabbix
  • 可視化
    • Kibana
    • Grafana
  • その他
    • AlertManager

具体的にこれらのコンポーネントを、どのように利用したのかという話をしたいと思います。

Hardware監視

ここでは、サーバのIPMIから得られるデータやネットワーク機器(SNMP)のデータの監視についてです。

今回は、Zabbixを利用しNTT中央研修センタにある合計11台のホストを監視しました。

使用したテンプレートも含め、以下のスクリーンショットを載せておきます。

IPMIからは、サーバのハードウェアの状態の監視を行いました。

IPMIのデータ取得は、Zabbixのデフォルトの設定では行えません。
ENVにZBX_IPMIPOLLERS=1 のように、0以上の値を設定することにより取得できるようになります。

            - name: ZBX_IPMIPOLLERS
              value: "1"

今回は準備期間中に、HDDの故障が発生したりしていましたので重要な監視になりました。

(Problemのメッセージ)

hardDisk [1] status major

また、ネットワーク機器はSNMPによる監視, メトリクスの取得を行いました。

後述する、GrafanaでZabbixで取得した、対外トラフィックの可視化を行いました。

サービス監視

主にPrometheusを利用し、Grafanaで可視化を行いました。

Prometheus

Prometheusでは Node-expoter, Ceph-expoter, BlackBox-expoterなどを利用しデータの収集を行いました。

Prometheusのデータの永続化についてです。
監視項目が多い場合かなりのストレージを使うため、注意が必要でした。

ICTSC2020の場合、10日間で20GBのストレージを消費しました。

また、PrometheusのDBのサイズが肥大化し起動に時間がかかるようになっていました。
長期の運用を考える場合に考え直さなければならない部分だと思っています。

Deploy

ICTSC2020ではArgoCDによるデプロイを行っています。
しかし、ConfigMapに書かれている設定の変更時にPodの再起動が行われません。

そのため、DeployのアノテーションにConfigMapのhash値をいれておき、ConfigMapに変更があったときのみPodのUpdateが行われるようにしました。

このような形です。

      annotations:
        checksum/config_volume: {{ $.Files.Get "templates/prometheus-configmap.yaml"| sha256sum }}
        checksum/blackbox_volume: {{ $.Files.Get "templates/blackbox_exporter-configmap.yaml"| sha256sum }}
        checksum/ceph_target_volume: {{ $.Files.Get "templates/ceph-exporter-target.yaml"| sha256sum }}
        checksum/node_volume: {{ $.Files.Get "templates/node_exporter-configmap.yaml"| sha256sum }}

Grafana

Grafanaでは、DatasourceにPrometheus, Zabbixを利用し可視化していました。

全体で3つGrafanaが存在したため、Dashboardのjsonをk8sのConfigMapで管理しています。

改善点

ICTSCではConfigMapの定義yamlに、すべてのdashboardのjsonが以下のように一つのファイルに書かれています。

---
apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: grafana-import-dashboards
  namespace: monitoring
data:
  grafana-net-2-dashboard.json: |
    {
      "__inputs": [{
        "name": "DS_PROMETHEUS",
        "label": "Prometheus",
        "description": "",
        "type": "datasource",
        "pluginId": "prometheus",
        "pluginName": "Prometheus"
      }],
      "__requires": [{
        "type": "panel",
        "id": "singlestat",
        "name": "Singlestat",
        "version": ""
      }, {
        "type": "panel",
        "id": "text",
        "name": "Text",
        "version": ""
      }, {
      .........

しかし、この状態だとdashboardの変更した際にConfigMapの定義ファイルとjsonが同じところにあるため書き換えが大変でした。

そのため、jsonとConfigMapの定義を分離することにしました。

apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: grafana-import-dashboards
  namespace: monitoring
data:
  grafana-net-2-dashboard.json: |-
{{ .Files.Get "dashboard/grafana-net-2-dashboard.json" | indent 4}}

templateに置かれているConfigMap定義から、別ディレクトリに置かれているjsonを読み込む形に変更しました。

その際にindent 4 をつけることによって、yamlに読み込まれた際のインデントを気にする必要がなくなります。

  k8s-dashboard.json: |-
{{ .Files.Get "dashboard/k8s-dashboard.json" | indent 4}}

また、サーバの監視にGrafana.comに公開されているdashboardを利用しました。

https://grafana.com/grafana/dashboards/11074

上にある公開されているDashboardの一部において、jsonの定義でConfigMapのサイズ制限を超えてしまう問題が発生しました。
GrafanaのAPIを利用して、Grafana.comから取得したデータのUploadを行い回避を行いました。

grafana_host="http://grafana:3000";
grafana_cred="${GF_ADMIN_USER}:${GF_ADMIN_PASSWORD}";
grafana_datasource="prometheus";
ds=(2842 1860 11074);
for d in "${ds[@]}"; do
  echo -n "Processing $d: ";
  j=$(curl -s -k -u "$grafana_cred" $grafana_host/api/gnet/dashboards/"$d" | jq .json);
  echo "{\"dashboard\":$j,\"overwrite\":true, \
        \"inputs\":[{\"name\":\"DS_PROMETHEUS\",\"type\":\"datasource\", \
        \"pluginId\":\"prometheus\",\"value\":\"$grafana_datasource\"}]}" \
  | curl -s -k -u "$grafana_cred" -XPOST -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    $grafana_host/api/dashboards/import -d "@-" ;
  echo "" ;
done

今回は行いませんでしたが、Grafana.comのようなPrivateなDashbordの公開場所を設けるなど、
k8sのConfigMapに書き込まない方がよいと感じました。

また、以前公開しているPomeriumの記事に関連してGrafanaの認証をGitHubで行えるようにしています。

以下のような設定をgrafana.iniに書くことによってGitHub OAuth2を有効にしました

    [auth.github]
    enabled = true
    allow_sign_up = true
    client_id = {{ .Values.github_clientid }}
    client_secret = {{ .Values.github_client_secret }}
    scopes = user:email,read:org
    auth_url = https://github.com/login/oauth/authorize
    token_url = https://github.com/login/oauth/access_token
    api_url = https://api.github.com/user
    team_ids = xxxxxxx
    allowed_organizations = ictsc

GitHub Organizationで利用している場合、team_idsという設定項目でGitHub Organizationの特定のTeamのみ利用できるといった指定をすることができます。

team_idsというものは、チーム名ではなくGithubAPIから取得できる数字のIDを書かなければなりません。

GitHubのwebからは取得できないため、curlで取得する必要があります。

GrafanaのSession管理

また、Grafanaはセッション管理などにSQLiteを利用しています。

Grafanaをレプリカを行って運用する場合にはDBのLockがかかってしまうことがあります。

具体的に、DBのLockがかかるとGrafanaから突然ログアウトされるような現象が発生します。

そのため、MySQLサーバなどを用意しgrafana.iniで以下のようにデータベース接続の設定を行うことで解消します。
databaseに接続情報を設定し、[session] をmysql変更します。

[database]
    # You can configure the database connection by specifying type, host, name, user and password
    # as seperate properties or as on string using the url propertie.
    # Either "mysql", "postgres" or "sqlite3", it's your choice
    type = mysql
    host = helm-grafana-mysql:3306
    name = grafana
    user = ictsc
    # If the password contains # or ; you have to wrap it with trippel quotes. Ex """#password;"""
    password = hogehoge
[session]
     # Either "memory", "file", "redis", "mysql", "postgres", default is "file"
     provider = mysql 

ネットワーク監視

次は、ネットワーク監視についてです。

主に、DataSourceはSNMP, sflowからデータを取得を行いました。

監視対象は、NTT中央研修センターにあるネットワーク機器です。
繰り返しになりますが、列挙しておきます。

  • ネットワーク機器
    • MX5
    • SRX1500
    • SN2410

これら3台の機器を、ZabbixからSNMPでデータ取得を行いました。

これらの機器のICTSC2020での使用用途を簡単に説明します。

  • MX5
    HomeNOC様とのBGPフルルートを受け取っているルータです。
    2拠点と接続し冗長化を行っています。
  • SRX1500
    MX5の配下に接続されている、Firewallです。
  • SN2410
    サーバ間の通信など、データ通信のコアスイッチとして利用しています。

これらの機器から取得したデータの可視化を行いました。

まず、HomeNOC様との対外トラフィックの可視化についてです。

以下は、取得したデータをZabbixのScreenで表示させたのものです。

今回はGrafanaで様々な監視のdashboardを扱っていますので、GrafanaでZabbixのデータを表示を行います。

GrafanaからZabbixのデータを表示するために、以下のPluginのインストールを行います。
https://grafana.com/grafana/plugins/alexanderzobnin-zabbix-app/

この際、zipファイルからインストール作業をする必要はなく、k8sのマニフェストからENVでインストールするプラグインを指定できます。

          - name: GF_INSTALL_PLUGINS
            value: "alexanderzobnin-zabbix-app"

今回、ZabbixをDatasourceとしたDashboardとして対外トラフィックの可視化を行いました。

以下は、本戦二日間の実際のDashboardになります。

Elastiflow

Elastiflowを利用したflow情報の可視化です。

構築はElastiflowのdocker-composeファイルを参考にk8sのtemplateを書いて構築をしました。
https://github.com/robcowart/elastiflow/blob/master/docker-compose.yml

参考までに、作成したelastiflow-logstash用のファイルを載せておきます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: elastiflow
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: elastiflow
  replicas: 2
  template:
    metadata:
      labels:
        app: elastiflow
    spec:
      containers:
        - name: elastiflow
          image: robcowart/elastiflow-logstash:4.0.1
          env:
            - name: LS_JAVA_OPTS
              value: "-Xms3g -Xmx3g"
            - name: ELASTIFLOW_AGENT_ID
              value: "elastiflow"
            - name: ELASTIFLOW_GEOIP_CACHE_SIZE
              value: "16384"
            - name: ELASTIFLOW_GEOIP_LOOKUP
              value: "true"
            - name: ELASTIFLOW_ASN_LOOKUP
              value: "true"
            - name: ELASTIFLOW_OUI_LOOKUP
              value: "true"
            - name: ELASTIFLOW_POPULATE_LOGS
              value: "true"
            - name: ELASTIFLOW_KEEP_ORIG_DATA
              value: "true"
            - name: ELASTIFLOW_DEFAULT_APPID_SRCTYPE
              value: "__UNKNOWN"
            - name: ELASTIFLOW_RESOLVE_IP2HOST
              value: "true"
            - name: ELASTIFLOW_NAMESERVER
              value: "127.0.0.1"
            - name: ELASTIFLOW_DNS_HIT_CACHE_SIZE
              value: "25000"
            - name: ELASTIFLOW_DNS_HIT_CACHE_TTL
              value: "900"
            - name: ELASTIFLOW_DNS_FAILED_CACHE_SIZE
              value: "75000"
            - name: ELASTIFLOW_DNS_FAILED_CACHE_TTL
              value: "3600"
            - name: ELASTIFLOW_ES_HOST
              value: "elasticsearch:9200"
            - name: ELASTIFLOW_NETFLOW_IPV4_PORT
              value: "2055"
            - name: ELASTIFLOW_NETFLOW_UDP_WORKERS
              value: "4"
            - name: ELASTIFLOW_NETFLOW_UDP_QUEUE_SIZE
              value: "4096"
            - name: ELASTIFLOW_NETFLOW_UDP_RCV_BUFF
              value: "33554432"

            - name: ELASTIFLOW_SFLOW_IPV4_PORT
              value: "6343"
            - name: ELASTIFLOW_SFLOW_UDP_WORKERS
              value: "4"
            - name: ELASTIFLOW_SFLOW_UDP_QUEUE_SIZE
              value: "4096"
            - name: ELASTIFLOW_SFLOW_UDP_RCV_BUFF
              value: "33554432"

            - name: ELASTIFLOW_IPFIX_UDP_IPV4_PORT
              value: "4739"
            - name: ELASTIFLOW_IPFIX_UDP_WORKERS
              value: "2"
            - name: ELASTIFLOW_IPFIX_UDP_QUEUE_SIZE
              value: "4096"
            - name: ELASTIFLOW_IPFIX_UDP_RCV_BUFF
              value: "33554432"
---
apiVersion: v1
kind: Service
metadata:
  name: elastiflow
  namespace: monitoring
  annotations:
    metallb.universe.tf/address-pool: privateIP
spec:
  ports:
    - name: netflow-port
      port: 2055
      protocol: UDP
      targetPort: 2055
    - name: sflow-port
      port: 6343
      protocol: UDP
      targetPort: 6343
    - name: ipfix-port
      port: 4739
      protocol: UDP
      targetPort: 4739
  selector:
    app: elastiflow
  type: LoadBalancer

ElastiflowのDashboardは以下にあるjsonファイルをKibanaからimportを行うことによって、みれるようになります。

https://github.com/robcowart/elastiflow/tree/master/kibana

コンテスト中のflow数はこのような形です。

監視基盤については以上です。

終わりに

今回k8sをどのように利活用したかについて説明しました。これが今回の我々の成果になります!
前編・後編と説明しましたが面白く読んでもらえましたでしょうか?
最後になりますが参加してくださった皆さん、スポンサーとしてリソース提供をしてくださったさくらインターネット様ありがとうございました。

もし今回の記事が皆さんがk8sを運用するにあたっての参考になれば幸いです。